本篇是實作常用的 AWS CloudFront 服務之 Terraform 模組,並且會使用到 YAML 資料結構來定義模組的內容,完整的專案程式碼分享在我的 Github 上。
./configs/cloudfront/distributions.yaml
與 模組 my_cloudfront
的放置位置 modules/my_cloudfront
:├── configs
│ ├── iam
│ │ ├── assume_role_policies
│ │ ├── policies
│ │ ├── role_policies
│ │ ├── user_policies
│ │ └── iam.yaml
│ ├── cloudfront
│ │ └── distributions.yaml
│ ├── s3
│ │ ├── policies
│ │ └── s3.yaml
│ ├── subnet
│ │ └── my-subnets.yaml
│ └── vpc
│ └── my-vpcs.yaml
├── example.tfvars
├── locals.tf
├── main.tf
├── modules
│ ├── my_cloudfront
│ │ ├── aws_cloudfront_response_headers_policy.tf
│ │ ├── cloudfront_distribution.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ └── variables.tf
│ ├── my_eips
│ ├── my_eips
│ ├── my_iam
│ ├── my_igw
│ ├── my_instances
│ ├── my_nacls
│ ├── my_route_tables
│ ├── my_s3
│ ├── my_subnets
│ └── my_vpc
└── variables.tf
./configs/cloudfront/distributions.yaml
內容來定義 CloudFront 需要用建立的資源:distributions:
- alias: "<ALIAS_NAME>"
department: "<DEPARTMENT_NAME>"
project: "<PROJECT_NAME>"
bucket_name: "<BUCKET_NAME>"
origin_path: ""
logging_bucket: ""
logging_prefix: ""
response_headers_policy: "<RESPONSE_HEADERS_POLICY_NAME>"
custom_error_response:
- error_caching_min_ttl: "10"
error_code: "403"
response_code: "200"
response_page_path: "/index.html"
- error_caching_min_ttl: "10"
error_code: "404"
response_code: "200"
response_page_path: "/index.html"
response_headers_policies:
- name: "<CUSTOM_SECURITY_HEADERS_POLICY_NAME>"
custom_headers_configs:
- header: "Cache-Control"
override: true
value: "no-store"
security_headers_config:
content_security_policy:
content_security_policy: "<CONTENT_SECURITY_POLICY>"
override: true
content_type_options:
override: true
frame_options:
frame_option: "SAMEORIGIN"
override: true
referrer_policy:
override: true
referrer_policy: "strict-origin-when-cross-origin"
strict_transport_security:
access_control_max_age_sec: 31536000
include_subdomains: true
override: true
preload: true
my_cloudfront
模組:./modules/my_cloudfront/outputs.tf
:output "distributions" {
value = aws_cloudfront_distribution.distributions
}
./modules/my_cloudfront/provider.tf
:provider "aws" {
region = var.aws_region
profile = var.aws_profile
}
./modules/my_cloudfront/variables.tf
:variable "aws_region" {
description = "AWS region"
default = "ap-northeast-1"
}
variable "aws_profile" {
description = "AWS profile"
default = ""
}
variable "environment" {
type = string
default = ""
}
variable "access_identity_path" {
type = string
default = ""
}
variable "acm_certificate_arn" {
type = string
default = ""
}
variable "distribution_path" {
type = string
default = ""
}
variable "s3_buckets" {
type = map(any)
default = {}
}
./modules/my_cloudfront/cloudfront_distribution.tf
:locals {
distributions = yamldecode(file("${var.distribution_path}"))["distributions"]
}
resource "aws_cloudfront_distribution" "distributions" {
for_each = { for r in local.distributions : r.name => r }
aliases = each.value.aliases
dynamic "custom_error_response" {
for_each = lookup(each.value, "custom_error_response", [])
content {
error_caching_min_ttl = custom_error_response.value.error_caching_min_ttl
error_code = custom_error_response.value.error_code
response_code = custom_error_response.value.response_code
response_page_path = custom_error_response.value.response_page_path
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
cached_methods = ["GET", "HEAD"]
compress = "true"
default_ttl = "0"
max_ttl = "0"
min_ttl = "0"
smooth_streaming = "false"
target_origin_id = "S3-${format("%s%s", each.value.bucket_name, each.value.origin_path)}"
viewer_protocol_policy = "redirect-to-https"
response_headers_policy_id = (each.value.response_headers_policy != "") ? aws_cloudfront_response_headers_policy.policies["${each.value.response_headers_policy}"].id : null
dynamic "lambda_function_association" {
for_each = lookup(each.value, "lambda_function_association", null) == null ? [] : [1]
content {
event_type = each.value.lambda_function_association.event_type
include_body = each.value.lambda_function_association.include_body
lambda_arn = each.value.lambda_function_association.lambda_arn
}
}
}
enabled = "true"
http_version = "http2"
is_ipv6_enabled = "true"
dynamic "logging_config" {
for_each = each.value.logging_bucket == "" ? [] : [1]
content {
bucket = "${each.value.logging_bucket}.s3.amazonaws.com"
include_cookies = "false"
prefix = each.value.logging_prefix
}
}
origin {
connection_attempts = "3"
connection_timeout = "10"
domain_name = "${each.value.bucket_name}.s3.amazonaws.com"
origin_id = "S3-${format("%s%s", each.value.bucket_name, each.value.origin_path)}"
origin_path = each.value.origin_path != "" ? each.value.origin_path : null
s3_origin_config {
origin_access_identity = var.access_identity_path
}
}
price_class = "PriceClass_All"
restrictions {
geo_restriction {
restriction_type = "none"
}
}
retain_on_delete = "false"
tags = {
Department = each.value.department
Env = var.environment
Name = "${format("%s%s", each.value.bucket_name, each.value.origin_path)}-cf"
Project = each.value.project
}
tags_all = {
Department = each.value.department
Env = var.environment
Name = "${format("%s%s", each.value.bucket_name, each.value.origin_path)}-cf"
Project = each.value.project
}
viewer_certificate {
acm_certificate_arn = var.acm_certificate_arn
cloudfront_default_certificate = "false"
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
depends_on = [
aws_cloudfront_response_headers_policy.policies,
var.s3_buckets
]
}
./modules/my_cloudfront/aws_cloudfront_response_headers_policy.tf
:locals {
response_headers_policies = yamldecode(file("${var.distribution_path}"))["response_headers_policies"]
}
resource "aws_cloudfront_response_headers_policy" "policies" {
for_each = { for r in local.response_headers_policies : r.name => r }
name = each.value.name
dynamic "custom_headers_config" {
for_each = lookup(each.value, "custom_headers_configs", null) != null ? [1] : []
content {
dynamic "items" {
for_each = each.value.custom_headers_configs
content {
header = items.value["header"]
override = items.value["override"]
value = items.value["value"]
}
}
}
}
dynamic "security_headers_config" {
for_each = lookup(each.value, "security_headers_config", null) != null ? [each.value.security_headers_config] : []
content {
dynamic "content_security_policy" {
for_each = lookup(security_headers_config.value, "content_security_policy", null) != null ? [security_headers_config.value["content_security_policy"]] : []
content {
content_security_policy = content_security_policy.value["content_security_policy"]
override = content_security_policy.value["override"]
}
}
dynamic "content_type_options" {
for_each = lookup(security_headers_config.value, "content_type_options", null) != null ? [security_headers_config.value["content_type_options"]] : []
content {
override = content_type_options.value["override"]
}
}
dynamic "frame_options" {
for_each = lookup(security_headers_config.value, "frame_options", null) != null ? [security_headers_config.value["frame_options"]] : []
content {
frame_option = frame_options.value["frame_option"]
override = frame_options.value["override"]
}
}
dynamic "referrer_policy" {
for_each = lookup(security_headers_config.value, "referrer_policy", null) != null ? [security_headers_config.value["referrer_policy"]] : []
content {
override = referrer_policy.value["override"]
referrer_policy = referrer_policy.value["referrer_policy"]
}
}
dynamic "strict_transport_security" {
for_each = lookup(security_headers_config.value, "strict_transport_security", null) != null ? [security_headers_config.value["strict_transport_security"]] : []
content {
access_control_max_age_sec = strict_transport_security.value["access_control_max_age_sec"]
include_subdomains = strict_transport_security.value["include_subdomains"]
override = strict_transport_security.value["override"]
preload = strict_transport_security.value["preload"]
}
}
}
}
}
example.tfvars
:aws_region="ap-northeast-1"
aws_profile="<YOUR_PROFILE>"
project_name="example"
department_name="SRE"
main.tf
:terraform {
required_providers {
aws = {
version = "5.15.0"
}
}
backend "s3" {
bucket = "<YOUR_S3_BUCKET_NAME>"
dynamodb_table = "<YOUR_DYNAMODB_TABLE_NAME>"
key = "terraform.tfstate"
region = "ap-northeast-1"
shared_credentials_file = "~/.aws/config"
profile = "<YOUR_PROFILE>"
}
}
# 其他模組省略...
# CloudFront 需要搭配與 S3 模組一起使用
# s3
module "s3" {
aws_profile = var.aws_profile
aws_region = var.aws_region
environment = var.environment
s3_path = "./configs/s3/s3.yaml"
source = "./modules/my_s3"
}
# cloudfront
module "cloudfront" {
aws_profile = var.aws_profile
aws_region = var.aws_region
acm_certificate_arn = "arn:aws:acm:us-east-1:188714232447:certificate/66dd6c2a-3482-474c-9dbb-befb925a0147"
access_identity_path = "origin-access-identity/cloudfront/XXXXXXXXXXXXXX"
distribution_path = "./configs/cloudfront/distributions.yaml"
s3_buckets = module.s3.s3_bucket
source = "./modules/my_cloudfront"
}
distributions:
- name: my-cloudfront
aliases:
- my-cloudfront.nextdrive.io
department: SRE
project: EXAMPLE
bucket_name: my-bucket
origin_path: ""
logging_bucket: ""
logging_prefix: ""
response_headers_policy: "NoStoreResponseHeader"
custom_error_response:
- error_caching_min_ttl: "10"
error_code: "403"
response_code: "200"
response_page_path: "/index.html"
- error_caching_min_ttl: "10"
error_code: "404"
response_code: "200"
response_page_path: "/index.html"
response_headers_policies:
- name: "NoStoreResponseHeaderWithCORS"
cors_config:
access_control_allow_credentials: false
access_control_allow_headers:
- "*"
access_control_allow_methods:
- "DELETE"
- "GET"
- "HEAD"
- "OPTIONS"
- "PATCH"
- "POST"
- "PUT"
access_control_allow_origins:
- "http://127.0.0.1:3000"
access_control_max_age_sec: 31536000
origin_override: true
custom_headers_configs:
- header: "Cache-Control"
override: true
value: "no-store"
terraform init && terraform plan --out .plan -var-file=example.tfvars
來確認一下結果:
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
+ create
Terraform will perform the following actions:
# module.cloudfront.aws_cloudfront_distribution.distributions["my-cloudfront"] will be created
+ resource "aws_cloudfront_distribution" "distributions" {
+ aliases = [
+ "my-cloudfront.nextdrive.io",
]
+ arn = (known after apply)
+ caller_reference = (known after apply)
+ domain_name = (known after apply)
+ enabled = true
+ etag = (known after apply)
+ hosted_zone_id = (known after apply)
+ http_version = "http2"
+ id = (known after apply)
+ in_progress_validation_batches = (known after apply)
+ is_ipv6_enabled = true
+ last_modified_time = (known after apply)
+ price_class = "PriceClass_All"
+ retain_on_delete = false
+ staging = false
+ status = (known after apply)
+ tags = {
+ "Department" = "SRE"
+ "Env" = ""
+ "Name" = "my-bucket-cf"
+ "Project" = "EXAMPLE"
}
+ tags_all = (known after apply)
+ trusted_key_groups = (known after apply)
+ trusted_signers = (known after apply)
+ wait_for_deployment = true
+ custom_error_response {
+ error_caching_min_ttl = 10
+ error_code = 403
+ response_code = 200
+ response_page_path = "/index.html"
}
+ custom_error_response {
+ error_caching_min_ttl = 10
+ error_code = 404
+ response_code = 200
+ response_page_path = "/index.html"
}
+ default_cache_behavior {
+ allowed_methods = [
+ "GET",
+ "HEAD",
]
+ cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
+ cached_methods = [
+ "GET",
+ "HEAD",
]
+ compress = true
+ default_ttl = 0
+ max_ttl = 0
+ min_ttl = 0
+ response_headers_policy_id = (known after apply)
+ smooth_streaming = false
+ target_origin_id = "S3-my-bucket"
+ trusted_key_groups = (known after apply)
+ trusted_signers = (known after apply)
+ viewer_protocol_policy = "redirect-to-https"
}
+ origin {
+ connection_attempts = 3
+ connection_timeout = 10
+ domain_name = "my-bucket.s3.amazonaws.com"
+ origin_id = "S3-my-bucket"
+ s3_origin_config {
+ origin_access_identity = "origin-access-identity/cloudfront/XXXXXXXXXXXXXX"
}
}
+ restrictions {
+ geo_restriction {
+ locations = (known after apply)
+ restriction_type = "none"
}
}
+ viewer_certificate {
+ acm_certificate_arn = "arn:aws:acm:us-east-1:188714232447:certificate/66dd6c2a-3482-474c-9dbb-befb925a0147"
+ cloudfront_default_certificate = false
+ minimum_protocol_version = "TLSv1.2_2021"
+ ssl_support_method = "sni-only"
}
}
# module.cloudfront.aws_cloudfront_response_headers_policy.policies["NoStoreResponseHeaderWithCORS"] will be created
+ resource "aws_cloudfront_response_headers_policy" "policies" {
+ etag = (known after apply)
+ id = (known after apply)
+ name = "NoStoreResponseHeaderWithCORS"
+ cors_config {
+ access_control_allow_credentials = false
+ access_control_max_age_sec = 31536000
+ origin_override = true
+ access_control_allow_headers {
+ items = [
+ "*",
]
}
+ access_control_allow_methods {
+ items = [
+ "DELETE",
+ "GET",
+ "HEAD",
+ "OPTIONS",
+ "PATCH",
+ "POST",
+ "PUT",
]
}
+ access_control_allow_origins {
+ items = [
+ "http://127.0.0.1:3000",
]
}
}
+ custom_headers_config {
+ items {
+ header = "Cache-Control"
+ override = true
+ value = "no-store"
}
}
}
# module.s3.aws_s3_bucket.s3_bucket["my-bucket"] will be created
+ resource "aws_s3_bucket" "s3_bucket" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "my-bucket"
+ bucket_domain_name = (known after apply)
+ bucket_prefix = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ tags = {
+ "Department" = "The department of bucket"
+ "Name" = "my-bucket"
+ "Project" = ""
}
+ tags_all = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
}
# module.s3.aws_s3_bucket_lifecycle_configuration.configurations["my-bucket"] will be created
+ resource "aws_s3_bucket_lifecycle_configuration" "configurations" {
+ bucket = (known after apply)
+ id = (known after apply)
+ rule {
+ id = "delete-index"
+ status = "Disabled"
+ expiration {
+ days = 3
+ expired_object_delete_marker = false
}
+ filter {
+ prefix = "some-prefix/"
}
}
}
# module.s3.aws_s3_bucket_policy.s3_bucket_policy["my-bucket"] will be created
+ resource "aws_s3_bucket_policy" "s3_bucket_policy" {
+ bucket = "my-bucket"
+ id = (known after apply)
+ policy = jsonencode(
{
+ Id = "PolicyForMyBucket"
+ Statement = []
+ Version = "2008-10-17"
}
)
}
# module.s3.aws_s3_bucket_server_side_encryption_configuration.configurations["my-bucket"] will be created
+ resource "aws_s3_bucket_server_side_encryption_configuration" "configurations" {
+ bucket = (known after apply)
+ id = (known after apply)
+ rule {
+ bucket_key_enabled = false
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "AES256"
}
}
}
# module.s3.aws_s3_bucket_versioning.versionings["my-bucket"] will be created
+ resource "aws_s3_bucket_versioning" "versionings" {
+ bucket = (known after apply)
+ id = (known after apply)
+ versioning_configuration {
+ mfa_delete = (known after apply)
+ status = "Enabled"
}
}
Plan: 7 to add, 0 to change, 0 to destroy.
───────────────────────────────────────────────────────────────────────
Saved the plan to: .plan
To perform exactly these actions, run the following command to apply:
terraform apply ".plan"
Releasing state lock. This may take a few moments...
下一篇文章將會展示實作 AWS CloudWatch 之 Terraform 模組。